package com.uxsino.simo.collector.connections;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.NoRouteToHostException;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.uxsino.simo.connections.AbstractConnection;
import com.uxsino.simo.connections.IFileConnection;
import com.uxsino.simo.connections.exception.SimoConnectionException;
import com.uxsino.simo.connections.exception.SimoQueryException;
import com.uxsino.simo.connections.target.SSHTarget;

public class SSHConnection<T extends SSHTarget> extends AbstractConnection<SSHTarget>
    implements IFileConnection<SSHTarget> {

    private static Logger logger = LoggerFactory.getLogger(SSHConnection.class);

    private final static String CHANNEL_TYPE = "shell";

    private final static String CHANNEL_PTY_TYPE = "dumb";

    private final static int CONN_TIME_OUT = 50000;

    private final static int SESSION_TIME_OUT = 900000;

    private final static String CHARSET_DETECT_CMD = "locale charmap";

    private final static String UTF_8 = "UTF-8";

    private final static String LINE_DELIMITER = "((\\n\\r)|(\\r\\n)){1}|(\\r){1}|(\\n){1}";

    private String charsetName = null;

    private String prompt = null;

    private String commandPath = "";

    private JSch jsch;

    private Session session;

    private ChannelShell openChannel;

    private ByteArrayOutputStream fromRemote;

    private PrintStream toRemote;

    private static TcpSessionManager sessionManager = new TcpSessionManager();

    private Properties config;

    public SSHConnection() {
        jsch = new JSch();
        config = new Properties();
        config.put("StrictHostKeyChecking", "no");
        config.put("PreferredAuthentications", "password,keyboard-interactive,publickey,gssapi-with-mic");
    }

    @Override
    public boolean isConnected() {
        connected = openChannel != null && openChannel.isConnected();
        return connected;
    }

    @Override
    public Object buildCmd(String cmdPattern, Map<String, String> args) {
        return cmdPattern;
    }

    @Override
    public int close() {
        try {
            try {
                if (fromRemote != null) {
                    fromRemote.close();
                }
            }catch (Exception e){}
            try {
                if (toRemote != null) {
                    toRemote.close();
                }
            }catch (Exception e){}
            try {
                if (openChannel != null) {
                    openChannel.disconnect();
                }
            }catch (Exception e){}
            try {
                if (session != null && session.isConnected()) {
                    session.disconnect();
                }
            }catch (Exception e){}
        } catch (Exception e) {
            logger.warn("Error closing connection: " + "", e);
        } finally {
            session = null;
            connected = false;
            fromRemote = null;
            toRemote = null;
        }
        super.close();
        return 0;
    }

    /***
     * wait minimum time between two session for same ip/port
     * 
     * 
     * @param target
     */
    private static void waitForSession(SSHTarget target) {
        sessionManager.waitFor(target.host, target.port);
    }

    @Override
    public int connect(SSHTarget target) {
        int timeout = CONN_TIME_OUT>target.getTimeout()?CONN_TIME_OUT:target.getTimeout();
        super.connect(target);
        state = 0;
        // sleep between connections to avoid socket error
        waitForSession(target);
        connected = false;
        commandPath = "";
        if (target.commandPath != null) {
            commandPath = processCommandPath(target.commandPath.trim());
        }
        try {
            session = jsch.getSession(target.getUsername(), target.host, target.port);
            if (session == null) {
                return state;
            }
            session.setPassword(target.getPassword());
            session.setConfig(config);
            session.setTimeout(SESSION_TIME_OUT);
            session.connect(timeout);
            if (!session.isConnected()) {
                return state;
            }

            openChannel = (ChannelShell) session.openChannel(CHANNEL_TYPE);
            // fromRemote = openChannel.getInputStream();
            toRemote = new PrintStream(openChannel.getOutputStream());
            fromRemote = new ByteArrayOutputStream();
            openChannel.setOutputStream(fromRemote);
            openChannel.setPtyType(CHANNEL_PTY_TYPE);
            openChannel.connect(timeout);
            openChannel.setPtySize(20000, 20000, 20000, 20000);
            connected = true;

            preprocessCharset();
        } catch (JSchException | IOException | SimoQueryException | SimoConnectionException je) {
            Throwable cause = je.getCause();

            if (cause instanceof NoRouteToHostException) {
                logger.error("not route to host for ssh connection. {}:{}", target.host, target.port);
            } else {
                logger.error("failed to connect on ssh: ", je);
            }
            close();
        }
        state = 1;
        return state;
    }

    @Override
    public Object execCmd(Object cmdPattern) throws SimoQueryException, SimoConnectionException {
        if (!isConnected()) {
            logger.error("can not exec command for it is not connected");
            throw new SimoConnectionException("no connection valid.");
        }

        fromRemote.reset();
        StringBuffer sb = new StringBuffer();
        String cmdStr = (String) cmdPattern;
        if (cmdStr.startsWith("#commandPath#")) {
            cmdStr = cmdStr.replaceFirst("#commandPath#", commandPath);
        }
        sb.append(cmdStr);
        sb.append("\n");
        toRemote.print(sb.toString());
        toRemote.flush();

        if (prompt == null || charsetName == null) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                logger.error("thread error while waiting", e);
            }
            String r = null;
            try {
                r = fromRemote.toString(UTF_8).trim();
            } catch (UnsupportedEncodingException e) {
                logger.error("unsupported charset error ", e);
            }
            fromRemote.reset();
            return r;
        }
        String result = null;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            logger.error("thread error while waiting", e);
        }
        try {
            result = fromRemote.toString(charsetName).trim();
        } catch (UnsupportedEncodingException e) {
            logger.error("unsupported charset error ", e);
            throw new SimoQueryException(e);
        }

        long startTime = System.currentTimeMillis();
        while (!result.endsWith(prompt)) {
            try {
                result = fromRemote.toString(charsetName).trim();
            } catch (UnsupportedEncodingException e) {
                logger.error("unsupported charset error ", e);
                throw new SimoQueryException(e);
            }
            if (!result.endsWith(prompt)) {
                long endTime = System.currentTimeMillis();
                if (endTime - startTime > SESSION_TIME_OUT) {
                    logger.error("session time out");
                    throw new SimoConnectionException("session time out");
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    logger.error("thread error while waiting", e);
                    break;
                }
            }
        }

        String[] lines = result.split(LINE_DELIMITER);
        sb.setLength(0);
        for (int i = 1; i < lines.length - 1; i++) {
            sb.append(lines[i]);
            sb.append("\n");
        }
        if (sb.length() > 0) {
            sb.setLength(sb.length() - 1);
        }
        result = sb.toString();
        fromRemote.reset();
        // logger.info("+++++++++++++++++++cmd: {}\tresult:\n{}", cmdStr, result);
        return result;
    }

    @Override
    public int supportedFeatures() {
        return 0;
    }

    @Override
    public boolean upload(String remotePath, String fileName, InputStream fileToUpload) {
        return false;
    }

    @Override
    public InputStream download(String remotePath, String fileName) {
        return null;
    }

    @Override
    public void connectForFileTrans(SSHTarget target) {

        connect(target);

    }

    @Override
    public boolean testWithConnected(String cmdString, String resStart) {
        if (cmdString != null && cmdString.startsWith("orclRac:")) {
            connected = false;
            String cmdStr = cmdString.substring(8);
            String cmdRes = null;
            try {
                cmdRes = (String) execCmd(cmdStr);
            } catch (SimoConnectionException | SimoQueryException e) {
                logger.error("Query by ssh failed: {}", e);
            }
            // close();
            if (cmdRes != null && cmdRes.startsWith(resStart)) {
                logger.info("cmdRes connection result: true");
                connected = true;
            }
        }
        return connected;
    }

    private String processCommandPath(String path) {
        if (path.length() == 0) {
            return path;
        }
        StringBuilder sb = new StringBuilder(path);
        if (sb.charAt(sb.length() - 1) != '/') {
            sb.append("/");
        }
        return sb.toString();
    }

    private void preprocessCharset() throws SimoQueryException, SimoConnectionException {
        findPrompt();
        String res = (String) execCmd(CHARSET_DETECT_CMD);
        if (res == null) {
            return;
        }
        String[] lines = res.split(LINE_DELIMITER);
        if (lines == null || lines.length != 3) {
            charsetName = UTF_8;
        } else {
            charsetName = lines[1].trim().toUpperCase();
        }

        if (charsetName == null || charsetName.length() == 0 || charsetName.equals("\"\"")) {
            charsetName = UTF_8;
        }
    }

    private void findPrompt() throws SimoQueryException, SimoConnectionException {
        String echoStr = "helloWorld";
        String pat = ".*(#|\\$|%|>).*";
        Pattern pattern = Pattern.compile(pat, Pattern.DOTALL);
        for (int i = 0; i < 3 && prompt == null; i++) {
            String testResult = (String) execCmd("echo " + echoStr);
            if (testResult == null) {
                logger.error("exec prompt cmd is null, cannot find prompt");
                continue;
            }
            int index = testResult.length() - 1;
            while (index >= 0 && (testResult.charAt(index) == '\r' || testResult.charAt(index) == '\n'
                    || testResult.charAt(index) == ' ')) {
                index--;
            }

            Matcher m = pattern.matcher(testResult);
            if (m.find()) {
                prompt = m.group(1);
                break;
            }

        }

        if (prompt == null) {
            throw new SimoQueryException("ssh connection cannot find prompt");
        }
    }
}
